// ==UserScript== // @name SuperK - 键盘刷视频(受控端) // @namespace http://tampermonkey.net/ // @version 0.1 // @description 控制端监听按键,受控端接收按键消息后控制网页内容。用法举例:打游戏时在不切屏的情况下控制抖音网页端的上一个视频,下一个视频,播放和停止。 // @author beibeibeibei // @license MIT // @match https://www.douyin.com/* // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAdZJREFUeF7tmk1OwzAQhWeg7FFPkEoUiVtwlHaZXgJ6Cbpsb8IxkACpOUHEnipGgU1rBM+OQf7J6/b5Z+abN04qR2XkPx15/kIAdMDICbAFRm4AHoJsAbbAyAmwBVwMcLl5qs7k4k5MdyuilcucdMaYxog2Rg/Lt/qmseOCDvhM3kz26SQ0PJJODzMbAgQw3bxuxZjF8G0Tmqly39bz9XFEGMDDi0koheBQ2tX8JGdvAPYCwRH98wJTq4AEQAewBf72DEA9lroefAimniCKjwDQUwgRzF0PdgACGFtHBSIAVCFEEM2PraP46QBUIUTQ1u317Hdv3/XQ/ND9gh1AAODvMqpgqE4HWAR8gQa3ADpDYuvozCEAVCFEEM2PraP46QBUIUTQV0f7Id33kEPxBTsAbYDeE1DCoY85FB8BoAoggr462g/pybUACji2jgoU3AKxE0T7EwBvhk7vNgdcjT3v8/sm4KfGME27up4dq/gMKOp6XHdtfbX0AvD1gcT5Y/4u+F79HgR0QD+oh6BmslUxVZYgVHedvK8HfSKDHjO5604OyD3J3+IngJKr65IbHeBCqeQxdEDJ1XXJjQ5woVTyGDqg5Oq65EYHuFAqeQwdUHJ1XXL7AKF/3lBxkE7hAAAAAElFTkSuQmCC // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // ==/UserScript== (function() { 'use strict'; // 添加样式 function add_css() { GM_addStyle(` .beibeibeibei-panel-warpper { width: 310px; height: 190px; position: fixed; top: 0; left: 0; display: flex; transition-timing-function: linear(0 0%, 0 1.8%, 0.01 3.6%, 0.03 6.35%, 0.07 9.1%, 0.13 11.4%, 0.19 13.4%, 0.27 15%, 0.34 16.1%, 0.54 18.35%, 0.66 20.6%, 0.72 22.4%, 0.77 24.6%, 0.81 27.3%, 0.85 30.4%, 0.88 35.1%, 0.92 40.6%, 0.94 47.2%, 0.96 55%, 0.98 64%, 0.99 74.4%, 1 86.4%, 1 100%); transition-duration: .3s; z-index: 1061; } .beibeibeibei-panel-warpper svg { vertical-align: initial; } .corner-box-warpper { .corner-box { background: transparent; opacity: 0; width: 310px; height: 190px; position: fixed; pointer-events: none; } .topright { z-index: -99999; top: 0; right: 0; } .bottomright { z-index: -99999; bottom: 0; right: 0; } .bottomleft { z-index: -99999; bottom: 0; left: 0; } .topleft { z-index: -99999; top: 0; left: 0; } } .beibeibeibei-panel-border { background: black; width: fit-content; height: fit-content; border-radius: 6px; } @keyframes movebackground { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } } .beibeibeibei-panel-border.running { background: linear-gradient(45deg, rgb(84 58 181), rgb(84 58 181 / 70%), rgb(0 172 193 / 70%), rgb(87 228 246), rgb(255 255 255)); animation: movebackground 4s infinite linear; background-size: 400%; } #beibeibeibei-panel { box-sizing: content-box; width: 300px; height: 180px; display: flex; flex-direction: column; background: black; padding: 2px; border: 1px solid white; border-radius: 6px; overflow: hidden; interpolate-size: allow-keywords; margin: 2px; transition-duration: 0.3s; transition-timing-function: linear(0 0%, 0 1.8%, 0.01 3.6%, 0.03 6.35%, 0.07 9.1%, 0.13 11.4%, 0.19 13.4%, 0.27 15%, 0.34 16.1%, 0.54 18.35%, 0.66 20.6%, 0.72 22.4%, 0.77 24.6%, 0.81 27.3%, 0.85 30.4%, 0.88 35.1%, 0.92 40.6%, 0.94 47.2%, 0.96 55%, 0.98 64%, 0.99 74.4%, 1 86.4%, 1 100%); } #beibeibeibei-panel .titlebar { display: flex; justify-content: flex-end; -webkit-user-select: none; user-select: none; container-name: titlebar; container-type: inline-size; } #beibeibeibei-panel .titlebar>* { width: 22px; height: 22px; padding: 0; overflow: hidden; } #beibeibeibei-panel .titlebar>*:not(:last-child) { margin-right: 2px; } @container titlebar (max-width: 120px) { /* 父容器宽度不够时不显示 */ #beibeibeibei-panel .titlebar>*:not(:last-child) { display: none; } } #beibeibeibei-panel .titlebar>button { cursor: pointer; } #beibeibeibei-panel .titlebar>.led { border: 1px solid #767676; background-color: #efefef; border-radius: 3px; box-sizing: border-box; cursor: help; overflow: hidden; } @supports (rx: 0) { #beibeibeibei-panel .titlebar>.led ellipse { transform-origin: 10px 10px; fill: gray; rx: 7; ry: 1; transform: rotate(120deg); transition: all 0.3s ease-out; } } @supports not (rx: 0) { #beibeibeibei-panel .titlebar>.led ellipse { transform-origin: 10px 10px; fill: gray; transform: rotate(120deg) scaleX(1.4) scaleY(0.2); transition: all 0.3s ease-out; } } @keyframes flicker { 0%, 100% { transform: rotate(120deg) scale(1); } 50% { transform: rotate(120deg) scale(1.2); } } @supports (rx: 0) { #beibeibeibei-panel .titlebar>.led.websocket-connected ellipse { animation: flicker 1.5s infinite; fill: #44ff00; rx: 5; ry: 5; } } @supports not (rx: 0) { #beibeibeibei-panel .titlebar>.led.websocket-connected ellipse { animation: flicker 1.5s infinite; fill: #44ff00; } } #beibeibeibei-panel .titlebar>.min .vline { opacity: 0; } #beibeibeibei-panel .msgs { flex-grow: 1; margin: 4px 0px; background: #2d2d2d; overflow-y: auto; } #beibeibeibei-panel .msgs .info, #beibeibeibei-panel .msgs .msg, #beibeibeibei-panel .msgs .error { color: #efefef; font-size: 9px; margin: 0; } #beibeibeibei-panel .msgs .error { color: #87ceeb; } #beibeibeibei-panel .footer { display: flex; height: 30px; min-height: 30px; justify-content: space-between; } #beibeibeibei-panel .footer>* { width: calc(50% - 2px); box-sizing: border-box; text-align: center; font-family: Arial, sans-serif; } #beibeibeibei-panel .footer>input { background: white; border: 2px solid gray; border-radius: 4px; } #beibeibeibei-panel .footer>.start { cursor: pointer; -webkit-user-select: none; user-select: none; white-space: nowrap; } /*隐藏输入框箭头*/ .beibeibeibei-panel-warpper input::-webkit-outer-spin-button, .beibeibeibei-panel-warpper input::-webkit-inner-spin-button { -webkit-appearance: none !important; margin: 0; } @supports (-moz-appearance:none) { @-moz-document url-prefix() { .beibeibeibei-panel-warpper input[type="number"] { -moz-appearance: textfield; appearance: none; } } } #beibeibeibei-panel-rule-dialog { position: fixed; top: 50%; transform: translateY(round(-50%, 1px)); border: none; border-radius: 6px; margin: 0 auto; padding: 0; box-shadow: 1px 1px 5px 2px white; &::backdrop { /* Safari 9+ */ -webkit-backdrop-filter: blur(1px); backdrop-filter: blur(1px); } .title { width: 100%; height: 31px; background-color: black; display: flex; flex-direction: row-reverse; border-radius: 6px 6px 0 0; border-bottom: 1px solid #555; .close { width: 46px; height: 100%; background-color: black; color: white; border-radius: 0 6px 0 0; border: none; cursor: pointer; outline: none; &:focus { border: 1px solid white; } svg { transition: all 0.1s; line { transition: all 0.1s; transform-origin: center; } } &:hover { background-color: #0F0000; svg { transform: rotate(90deg); } } &:active { border: 1px solid darkred; svg { line { transform: rotate(90deg); stroke: red; } } } } } .table { min-width: 180px; min-height: 100px; background-color: #222; color: white; border-radius: 0 0 6px 6px; padding: 10px; font-size: 12px; .column { display: flex; height: 40px; background-color: black; >* { width: 100px; overflow: hidden; display: flex; justify-content: center; align-items: center; } >*:not(:last-child) { border-right: 1px solid gray; } } .column.header { border-radius: 6px 6px 0 0; border: 1px solid gray; >*:last-of-type { /*不选中规则删除这个格子*/ -webkit-user-select: none; user-select: none; } } .column.body { border: 1px solid gray; border-top: none; button.remove { background-color: #111; color: white; border: 1px solid white; border-radius: 4px; cursor: pointer; -webkit-user-select: none; user-select: none; &:hover { border-color: #BBB; box-shadow: inset 0px 0px 2px 0px white; } &:active { border-color: #888; color: #888; transform: scale(0.95); } } } .column:last-of-type { border-radius: 0 0 6px 6px; } .add { width: 100%; height: 40px; cursor: pointer; background-color: black; color: white; border: 1px solid white; border-radius: 6px; margin-top: 4px; -webkit-user-select: none; user-select: none; &:hover { box-shadow: inset 0px 0px 2px 0px white; } &:active { transform: translate(1px, 1px); width: calc(100% - 2px); } } } } #beibeibeibei-panel-add-rule-dialog { position: fixed; top: 50%; transform: translateY(round(-50%, 1px)); border: none; border-radius: 6px; margin: 0 auto; padding: 0; box-shadow: 0px 0px 5px 0px white; background-color: black; width: 470px; overflow: hidden; &::backdrop { /* Safari 9+ */ -webkit-backdrop-filter: blur(1px); backdrop-filter: blur(1px); } .title { width: 100%; height: 31px; background-color: black; display: flex; flex-direction: row-reverse; border-bottom: 1px solid #555; .close { width: 46px; height: 100%; background-color: black; color: white; border-radius: 0 6px 0 0; border: none; cursor: pointer; outline: none; &:focus { border: 1px solid white; } svg { transition: all 0.1s; line { transition: all 0.1s; transform-origin: center; } } &:hover { background-color: #0F0000; svg { transform: rotate(90deg); } } &:active { border: 1px solid darkred; svg { line { transform: rotate(90deg); stroke: red; } } } } } .add-panel { background-color: #222; color: white; padding: 10px; font-size: 12px; .add-panel-item { margin-bottom: 10px; input { background-color: #2d3436; color: #ecf0f1; border: 1px solid #34495e; border-radius: 4px; font-size: 12px; padding: 1px 3px; outline: none; text-align: center; vertical-align: top; &:focus { border: 1px solid white; } } input[type="number"] { width: 30px; } } .add-panel-item.text-center { text-align: center; } } .footer { display: flex; justify-content: space-around; padding: 10px; border-top: 1px solid #555; background: black; -webkit-user-select: none; user-select: none; button { background-color: #2d3436; color: #ecf0f1; border: 1px solid #34495e; border-radius: 4px; font-size: 12px; padding: 6px 12px; cursor: pointer; outline: none; transition: background-color 0.2s, transform 0.1s; &:hover { box-shadow: inset 0px 0px 5px 1px #3d4a51; } &:active { transform: translate(1px, 1px); background-color: #1a2023; } &:focus { border: 1px solid white; } } .add { background-color: #05150b; } .cancel { background-color: #170503; } } } /* 自定义select */ @keyframes custom-select-expand { 0% { display: none; height: 0px; overflow-y: auto; } 1% { display: block; overflow-y: hidden; } 99% { display: block; overflow-y: hidden; } 100% { display: block; height: auto; overflow-y: auto; } } .custom-select { position: relative; display: inline-block; .select-button { vertical-align: top; background-color: #2d3436; color: #ecf0f1; border: 1px solid #34495e; border-radius: 4px; padding: 0px 3px; padding-right: 20px; font-size: 12px; text-align: center; background-repeat: no-repeat; background-position: right 2px top 2px; outline: none; cursor: pointer; } .select-button:focus { border: 1px solid white; } .options-container { position: absolute; left: 0; width: 100%; font-size: 12px; background-color: #2d3436; border: none; border-radius: 4px; overflow-y: auto; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); z-index: 10; white-space: nowrap; height: 0px; interpolate-size: allow-keywords; } .options-container::-webkit-scrollbar { width: 6px; background: #202324; border-radius: 0 3px 3px 0; } .options-container::-webkit-scrollbar-thumb { background: #454a4d; border-radius: 3px; } .options-container::-webkit-scrollbar-thumb:hover { background: #575e62; } .option-item { background-color: #2d3436; color: #ecf0f1; padding: 4px 0px; text-align: center; cursor: pointer; } .option-item:hover { background: #777; } .option-item:focus { background: #777; } .option-item:focus-visible { outline: none; background: #777; } } /* 选项向上展开 */ .custom-select.position-above { .select-button { background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0 0 11 11'%3e%3cpolygon%20points='3,6 5,4 7,6'%20style='fill:white;stroke:white;stroke-width:1'/%3e%3c/svg%3e"); } .options-container { bottom: calc(100% + 1px); } } /* 选项向下展开 */ .custom-select.position-below { .select-button { background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0 0 11 11'%3e%3cpolygon%20points='3,4 5,6 7,4'%20style='fill:white;stroke:white;stroke-width:1'/%3e%3c/svg%3e"); } .options-container { top: calc(100% + 1px); } } /* 选项右侧展开 */ .custom-select.position-right { .select-button { background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0 0 11 11'%3e%3cpolygon%20points='4,6 6,4 4,2' style='fill:white;stroke:white;stroke-width:1'/%3e%3c/svg%3e"); } .options-container { top: calc(50%); left: calc(100% + 1px); transform: translateY(-50%); } } `); } // 动态插入 HTML 结构 function add_HTML() { const panelWrapper = document.createElement('div'); panelWrapper.className = 'beibeibeibei-panel-warpper'; document.body.appendChild(panelWrapper); panelWrapper.innerHTML = `
规则名
网址包含文字
选择器A
选择器A数量限制
选择器B
选择器B的第几项
对选择器B的操作
操作数据
规则删除
1.收到 消息
2.网站地址包含
3.选择器 的数量
无限制
最少
最多
等于
范围
~ 之间
满足上述条件时将执行第4条
4.选择器 的第 项会被
点击
旋转
缩放
`; } function beibeibeibei_init() { /* 关闭dialog1 */ document.querySelector("#beibeibeibei-panel-rule-dialog .title .close").addEventListener("click", (e) => { e.target.closest('dialog').parentElement.querySelectorAll('dialog').forEach((d) => { d.close(); }) }); /* 打开添加规则弹窗 */ document.querySelector("#beibeibeibei-panel-rule-dialog .table .add").addEventListener("click", (e) => { document.querySelector('#beibeibeibei-panel-add-rule-dialog').showModal(); }); /* 关闭dialog2 title */ document.querySelector("#beibeibeibei-panel-add-rule-dialog .title .close").addEventListener("click", (e) => { e.target.closest('dialog').close(); }); /* 关闭dialog2 返回*/ document.querySelector("#beibeibeibei-panel-add-rule-dialog .footer .cancel").addEventListener("click", (e) => { e.target.closest('dialog').close(); }); /* 打开规则弹窗 */ document.querySelector("#beibeibeibei-panel .rule").addEventListener("click", () => { /** @type {HTMLDialogElement} */ let dialog = document.querySelector("#beibeibeibei-panel-rule-dialog"); dialog.showModal(); }); /* 清空消息记录 */ document.querySelector("#beibeibeibei-panel .cls").addEventListener("click", () => { let msgs = document.querySelector("#beibeibeibei-panel .msgs"); msgs.replaceChildren(); }); // #region /* 缩小面板 */ let panelIsMin = false; document.querySelector("#beibeibeibei-panel .min").addEventListener("click", () => { panelIsMin = !panelIsMin; let vl = document.querySelector("#beibeibeibei-panel .min .vline"); vl.style.opacity = panelIsMin ? '1' : '0'; vl.parentElement.parentElement.title = panelIsMin ? "恢复面板" : "缩小面板"; let panel = document.querySelector("#beibeibeibei-panel"); panel.style.width = panelIsMin ? "22px" : "300px"; panel.style.height = panelIsMin ? "22px" : "180px"; }); // #endregion // #region /* 移动位置 */ let panelpos = 3; document.querySelector("#beibeibeibei-panel .move").addEventListener("click", () => { panelpos = (panelpos + 1) % 4; updatePos(); }); function updatePos() { const wrapper = document.querySelector("#beibeibeibei-panel").parentElement.parentElement; const positions = [getComputedStyle(document.querySelector(".beibeibeibei-panel-warpper .topright")), getComputedStyle(document.querySelector(".beibeibeibei-panel-warpper .bottomright")), getComputedStyle(document.querySelector(".beibeibeibei-panel-warpper .bottomleft")), getComputedStyle(document.querySelector(".beibeibeibei-panel-warpper .topleft"))]; wrapper.style.inset = positions[panelpos].inset; switch (panelpos) { case 0: wrapper.style.alignItems = "flex-start"; wrapper.style.justifyContent = "flex-end"; break; case 1: wrapper.style.alignItems = "flex-end"; wrapper.style.justifyContent = "flex-end"; break; case 2: wrapper.style.alignItems = "flex-end"; wrapper.style.justifyContent = "flex-start"; break; case 3: wrapper.style.alignItems = "flex-start"; wrapper.style.justifyContent = "flex-start"; break; } }; const debounce = (fn, delay) => { let timer; return function () { if (timer) { clearTimeout(timer); } timer = setTimeout(() => { fn(); }, delay); } }; const cancalDebounce = debounce(updatePos, 200); window.addEventListener('resize', cancalDebounce); // #endregion /* 按下 ESC 键时关闭弹窗 */ document.addEventListener('keydown', (event) => { if (event.key === 'Escape') { let dialog = document.querySelector("#beibeibeibei-panel-rule-dialog"); dialog.close(); let dialog2 = document.querySelector("#beibeibeibei-panel-add-rule-dialog"); dialog2.close(); } }); // #region /* add-rule-dialog弹窗循环tab键功能 */ const focusStart = document.querySelector("#beibeibeibei-panel-add-rule-dialog > div.focus-start") focusStart.addEventListener("focus", (event) => { let els = document.querySelector('#beibeibeibei-panel-add-rule-dialog').querySelectorAll("input, select, button"); els[els.length - 1].focus(); }); const focusEnd = document.querySelector("#beibeibeibei-panel-add-rule-dialog > div.focus-end"); focusEnd.addEventListener("focus", (event) => { let els = document.querySelector('#beibeibeibei-panel-add-rule-dialog').querySelectorAll("input, select, button"); els[0].focus(); }); // #endregion // #region /* 根据选中的选项显示或隐藏对应的元素 */ const quantitySelector = document.querySelector('#beibeibeibei-panel-add-rule-dialog .quantitySelector'); const inputSingle = document.querySelector('#beibeibeibei-panel-add-rule-dialog .add-panel-item .single-input'); const inputRange = document.querySelector('#beibeibeibei-panel-add-rule-dialog .add-panel-item .range-input'); inputSingle.style.display = 'none'; inputRange.style.display = 'none'; quantitySelector.addEventListener('change', function () { const selectedValue = this.querySelector('.select-button').getAttribute('data-value'); // 根据选择值显示对应输入 switch (selectedValue) { case '1': // 无限制 default: // 其他情况隐藏所有输入 inputSingle.style.display = 'none'; inputRange.style.display = 'none'; break; case '2': // 最少 case '3': // 最多 case '4': // 等于 inputSingle.style.display = 'inline'; inputRange.style.display = 'none'; break; case '5': // 范围 inputSingle.style.display = 'none'; inputRange.style.display = 'inline'; break; } }); // #endregion // #region /* 监听 dialog 关闭事件, 重置其中的内容 */ const dialog2 = document.querySelector('#beibeibeibei-panel-add-rule-dialog'); dialog2.addEventListener('close', () => { // 重置 dialog2.querySelectorAll('input').forEach(input => { input.value = ''; }); // 重置.custom-select组件 dialog2.querySelectorAll('.custom-select').forEach(customSelect => { const selectButton = customSelect.querySelector('.select-button'); const optionsContainer = customSelect.querySelector('.options-container'); selectButton.textContent = '请选择'; selectButton.removeAttribute('data-value'); optionsContainer.style.animation = 'none';// 移除现有动画 void optionsContainer.offsetWidth;// 强制触发重绘以重置动画 }); // 隐藏动态输入区域 dialog2.querySelectorAll('.single-input, .range-input').forEach(el => { el.style.display = 'none'; }); }); // #endregion // #region /* 自定义select处理 */ document.querySelectorAll('.custom-select').forEach(customSelect => { const selectButton = customSelect.querySelector('.select-button'); const optionsContainer = customSelect.querySelector('.options-container'); let isOpen = false; selectButton.addEventListener('click', function (e) { // 切换选项可见性 e.stopPropagation(); if (isOpen) { resetAndPlay(optionsContainer, 'reverse'); customSelect.querySelectorAll('.option-item').forEach(option => { option.setAttribute('tabindex', '-1'); }) } else { resetAndPlay(optionsContainer, 'normal'); customSelect.querySelectorAll('.option-item').forEach(option => { option.setAttribute('tabindex', '0'); }) } isOpen = !isOpen; }); customSelect.querySelectorAll('.option-item').forEach(option => { // 点击事件处理 option.addEventListener('click', function () { selectButton.textContent = this.textContent; selectButton.setAttribute('data-value', this.dataset.value); if (isOpen) { resetAndPlay(optionsContainer, 'reverse'); isOpen = false; } // 触发自定义的 change 事件 const changeEvent = new Event('change', { bubbles: true }); customSelect.dispatchEvent(changeEvent); }); // 键盘事件处理 option.addEventListener('keydown', function (e) { switch (e.key) { case 'Enter': case ' ':{ e.preventDefault(); this.click(); selectButton.focus(); break; } case 'ArrowDown':{ e.preventDefault(); const next = this.nextElementSibling || optionsContainer.firstElementChild; next.focus(); break; } case 'ArrowUp':{ e.preventDefault(); const prev = this.previousElementSibling || optionsContainer.lastElementChild; prev.focus(); break; } case 'ArrowLeft': case 'ArrowRight':{ // 按左键或右键时, 将焦点返回到select-button e.preventDefault(); selectButton.focus(); break; } } }); }); document.addEventListener('click', function () { // 点击页面其他区域关闭选项 if (isOpen) { resetAndPlay(optionsContainer, 'reverse'); isOpen = false; } }); optionsContainer.addEventListener('click', function (e) { // 防止选项容器点击时冒泡 e.stopPropagation(); }); selectButton.addEventListener('keydown', function (e) { switch (e.key) { case 'ArrowLeft': case 'ArrowRight': // 按左键或右键时, 将焦点切换到option里 if (isOpen) { e.preventDefault(); customSelect.querySelector('.option-item')?.focus(); } break; } }); }); function resetAndPlay(el, direction) { // 移除现有动画 el.style.animation = 'none'; // 强制触发重绘以重置动画 void el.offsetWidth; // 应用新动画设置 el.style.animation = `custom-select-expand 0.1s ease-out ${direction} forwards`; } // #endregion // #region /* 模拟输入按钮 */ let testBtn = document.querySelector("#beibeibeibei-panel-add-rule-dialog button.test"); testBtn.addEventListener('click', () => { const dialog2 = testBtn.closest("dialog"); // 填充文本输入框 dialog2.querySelector('.rule-name').value = '旋转视频示例'; dialog2.querySelector('.url-pattern').value = 'yin.com'; dialog2.querySelector('.count-based-selector').value = 'video'; dialog2.querySelector('.action-selector').value = 'video'; dialog2.querySelector('.action-index').value = '0'; // 设置数量条件选择器为"最多" const quantitySelector = dialog2.querySelector('.quantitySelector'); const quantityButton = quantitySelector.querySelector('.select-button'); quantityButton.textContent = '最多'; quantityButton.dataset.value = '3'; quantitySelector.dispatchEvent(new Event('change')); // 填充数量值 dialog2.querySelector('.quantity-value').value = '1'; // 设置操作类型为"点击" const actionTypeSelector = dialog2.querySelector('.action-type'); const actionButton = actionTypeSelector.querySelector('.select-button'); actionButton.textContent = '旋转'; actionButton.dataset.value = '2'; actionTypeSelector.dispatchEvent(new Event('change')); // 填充操作数值 dialog2.querySelector('.action-num').value = '90'; // 新增字段 }) // #endregion // #region /* 添加新条目 */ function collectRuleData() { let dialog2 = document.querySelector("#beibeibeibei-panel-add-rule-dialog"); return { ruleName: dialog2.querySelector('.rule-name').value.trim(), urlPattern: dialog2.querySelector('.url-pattern').value.trim(), selectorA: dialog2.querySelector('.count-based-selector').value.trim(), quantityCondition: dialog2.querySelector('.quantitySelector .select-button').textContent, quantityValue: getQuantityValues(dialog2), selectorB: dialog2.querySelector('.action-selector').value.trim(), actionIndex: dialog2.querySelector('.action-index').value.trim(), actionType: dialog2.querySelector('.action-type .select-button').textContent, actionNum: dialog2.querySelector('.action-num').value.trim() }; } function getQuantityValues(dialog) { const condition = dialog.querySelector('.quantitySelector .select-button').dataset.value; switch (condition) { case '2': // 最少 case '3': // 最多 case '4': // 等于 return dialog.querySelector('.quantity-value').value; case '5': // 范围 return `${dialog.querySelector('.min-quantity').value}-${dialog.querySelector('.max-quantity').value}`; default: return ''; } } function validateRuleData(data) { if (!data.ruleName) { alert("规则名称不能为空"); return false; } if (data.quantityCondition !== "无限制" && !data.selectorA) { alert("选择器A不能为空"); return false; } if (!data.selectorB) { alert("选择器B不能为空"); return false; } if (isNaN(data.actionIndex) || data.actionIndex < 0) { alert("索引值必须是非负数字"); return false; } return true; } function addRuleToStorage(ruleData) { const rules = GM_getValue('rulesData', []); const newRule = [ ruleData.ruleName, ruleData.urlPattern, ruleData.selectorA, formatQuantityCondition(ruleData.quantityCondition, ruleData.quantityValue), ruleData.selectorB, ruleData.actionIndex, ruleData.actionType, ruleData.actionNum ]; rules.push(newRule); GM_setValue('rulesData', rules); } function formatQuantityCondition(condition, value) { switch (condition) { case '无限制': return '无限制'; case '最少': return `最少 ${value} 个`; case '最多': return `最多 ${value} 个`; case '等于': return `等于 ${value} 个`; case '范围': return `范围 ${value} 个`; default: return '无限制'; } } function addRuleToUI(ruleData) { const ruleDialog = document.querySelector('#beibeibeibei-panel-rule-dialog'); const newBody = document.createElement('div'); newBody.className = 'column body'; const fields = [ ruleData.ruleName, ruleData.urlPattern, ruleData.selectorA, formatQuantityCondition(ruleData.quantityCondition, ruleData.quantityValue), ruleData.selectorB, ruleData.actionIndex, ruleData.actionType, ruleData.actionNum ]; fields.forEach(text => { const div = document.createElement('div'); div.textContent = text; newBody.appendChild(div); }); // 添加删除按钮 const deleteDiv = document.createElement('div'); const delBtn = document.createElement('button'); delBtn.className = 'remove'; delBtn.textContent = '删除此条'; delBtn.onclick = function () { const bodyElement = this.closest('.column.body'); const index = bodyElement.dataset.index; // 从页面中删除元素 bodyElement.remove(); // 从存储的数据中删除对应条目 const savedData = GM_getValue('rulesData', []); savedData.splice(index, 1); GM_setValue('rulesData', savedData); }; deleteDiv.appendChild(delBtn); newBody.appendChild(deleteDiv); // 插入到添加按钮之前 const addBtn = ruleDialog.querySelector('.add'); ruleDialog.querySelector('.table').insertBefore(newBody, addBtn); } let addBtn = document.querySelector("#beibeibeibei-panel-add-rule-dialog button.add"); addBtn.addEventListener('click', () => { const ruleData = collectRuleData(); if (validateRuleData(ruleData)) { addRuleToStorage(ruleData); addRuleToUI(ruleData); addBtn.closest("dialog").close(); } }); // #endregion }; function beibeibeibei_socket() { const startButton = document.querySelector("#beibeibeibei-panel .start"); const testButton = document.querySelector("#beibeibeibei-panel .test"); const portInput = document.querySelector("#beibeibeibei-panel input.port"); const pborder = document.querySelector(".beibeibeibei-panel-border"); const msgContainer = document.querySelector("#beibeibeibei-panel .msgs"); /** @type {HTMLElement} */ const led = document.querySelector("#beibeibeibei-panel .led"); // 获取端口号 const port = portInput.value.trim(); // 定义 WebSocket 的 URL const wsUrl = `ws://localhost:${port}`; // 定义 WebSocket 实例和定时器 /** @type {WebSocket} */ let ws; let websocketCheckInterval; let websocketCheckIntervalTime = 1000; let messageCleanupInterval; let messageCleanupIntervalTime = 5000; // isrunning只是建议启动, 和 WebSocket 状态无关 let isrunning = false; // 启动连接 startButton.addEventListener("click", () => { isrunning = !isrunning; // 修改UI if (isrunning) { startButton.textContent = '停止'; pborder.classList.add("running"); } else { startButton.textContent = '启动'; pborder.classList.remove("running"); } // 控制 Websocket if (isrunning) { // 如果 WebSocket 已经存在, 先关闭它 if (ws) { ws.close(); } // 创建新的 WebSocket 实例 ws = new WebSocket(wsUrl); // 设置定时器, 间隔 intervalTimeout 毫秒检查一次 WebSocket 状态 websocketCheckInterval = setInterval(() => { if (ws.readyState === WebSocket.CLOSING || ws.readyState === WebSocket.CLOSED) { console.log("WebSocket 状态为 CLOSING 或 CLOSED, 尝试重新连接..."); appendMessage("info", `尝试重新连接`); // 关闭当前 WebSocket 实例 ws.close(); // 创建新的 WebSocket 实例 ws = new WebSocket(wsUrl); // 添加 WebSocket 的事件监听 ws.onopen = handleWebSocketOpen; ws.onmessage = handleWebSocketMessage; ws.onerror = handleWebSocketError; ws.onclose = handleWebSocketClose; } }, websocketCheckIntervalTime); // 设置定时器, 控制消息容器中的子元素数量 messageCleanupInterval = setInterval(() => { const childrenCount = msgContainer.childElementCount; if (childrenCount > 50) { // 计算要删除的前 5% 子元素的数量 const deleteCount = Math.ceil(childrenCount * 0.05); // 删除前 5% 的子元素 requestIdleCallback(() => { for (let i = 0; i < deleteCount; i++) { if (msgContainer.childElementCount > 0) { msgContainer.removeChild(msgContainer.firstChild); } } }); } }, messageCleanupIntervalTime); // 添加 WebSocket 的事件监听 ws.onopen = handleWebSocketOpen; ws.onmessage = handleWebSocketMessage; ws.onerror = handleWebSocketError; ws.onclose = handleWebSocketClose; } else { // 停止定时器 if (websocketCheckInterval) { clearInterval(websocketCheckInterval); websocketCheckInterval = null; } // 停止定时器 if (messageCleanupInterval) { clearInterval(messageCleanupInterval); messageCleanupInterval = null; } // 关闭 WebSocket 连接 if (ws) { ws.close(); ws = null; } console.log("WebSocket 连接已停止"); } }); // WebSocket 连接成功 function handleWebSocketOpen() { console.log("WebSocket 连接成功"); appendMessage("info", `已连接 WebSocket 服务器:${wsUrl}`); led.classList.add("websocket-connected"); led.title = "连接状态: 已连接"; } // WebSocket 收到消息 function handleWebSocketMessage(/** @type { MessageEvent } */event) { console.log("收到消息:", event.data); appendMessage("msg", event.data); // 处理消息并执行规则 beibeibeibei_processRules(event.data); } // WebSocket 发生错误 function handleWebSocketError(event) { console.log("WebSocket 发生错误:", event); appendMessage("error", "WebSocket 连接错误"); } // WebSocket 连接关闭 function handleWebSocketClose(event) { console.log("WebSocket 已关闭"); appendMessage("info", `WebSocket 连接已断开`); led.classList.remove("websocket-connected"); led.title = "连接状态: 未连接"; } // 测试按钮点击事件 testButton.addEventListener("click", () => { if (ws && ws.readyState === WebSocket.OPEN) { // 发送测试消息 const testMessage = "测试消息"; ws.send(testMessage); appendMessage("info", `已发送测试消息:${testMessage}`); } else { appendMessage("error", "WebSocket 连接未开启, 无法发送测试消息"); } }); // 显示消息 function appendMessage(className, textContent) { const msg = document.createElement("p"); msg.className = className; msg.textContent = textContent; msgContainer.appendChild(msg); // 获取滚动条位置和容器高度信息 const isNearBottom = msgContainer.scrollTop + msgContainer.clientHeight >= msgContainer.scrollHeight * 0.9; // 如果滚动条接近底部,则滚动到底部 if (isNearBottom) { msgContainer.scrollTop = msgContainer.scrollHeight; } } } function runWhenDocumentReady(...funcs) { const validFuncs = [...new Set(funcs)].filter(item => typeof item === 'function'); if (document.readyState === 'loading') { validFuncs.forEach(func => document.addEventListener('DOMContentLoaded', func) ); } else { validFuncs.forEach(func => func()); } } // const gmStorage = { // rulesData: [ // ['msg1', '', 'button', '无限制', '.testA', '0', '旋转', '90'], // ['msg1', '', 'button', '无限制', '.testA', '0', '缩放', '1.1'], // ['msg1', '', 'button', '无限制', '.testA', '0', '点击', ''], // ['msg-test', '', '.test-element', '最少 6 个', '.test-element', '0', '旋转', '135'] // ] // }; // 保存规则数据 function beibeibeibei_saveRulesData() { const bodyElements = document.querySelectorAll('#beibeibeibei-panel-rule-dialog .column.body'); const savedData = []; bodyElements.forEach((element, index) => { const children = element.children; const rowData = []; for (let i = 0; i < children.length; i++) { if (i === children.length - 1) { // 跳过最后一个包含按钮的div continue; } rowData.push(children[i].textContent.trim()); } savedData.push(rowData); }); GM_setValue('rulesData', savedData); console.log('规则数据已保存'); } // 恢复规则数据 function beibeibeibei_restoreRulesData() { const savedData = GM_getValue('rulesData', []); const headerElement = document.querySelector('#beibeibeibei-panel-rule-dialog .column.header'); const addageElement = document.querySelector('#beibeibeibei-panel-rule-dialog .add'); const bodyElements = document.querySelectorAll('#beibeibeibei-panel-rule-dialog .column.body'); // 清除现有的body元素 bodyElements.forEach(element => { element.remove(); }); // 恢复保存的内容 savedData.forEach((rowData, index) => { const newBody = document.createElement('div'); newBody.className = 'column body'; newBody.dataset.index = index; if (rowData.length < 8) { rowData = rowData.concat([null]); } rowData.forEach((data) => { const div = document.createElement('div'); div.textContent = data || ''; newBody.appendChild(div); }); // 添加删除按钮 const deleteDiv = document.createElement('div'); const delBtn = document.createElement('button'); delBtn.className = 'remove'; delBtn.textContent = '删除此条'; delBtn.type = 'button'; delBtn.onclick = function () { const bodyElement = this.closest('.column.body'); const index = bodyElement.dataset.index; // 从页面中删除元素 bodyElement.remove(); // 从存储的数据中删除对应条目 const savedData = GM_getValue('rulesData', []); savedData.splice(index, 1); GM_setValue('rulesData', savedData); }; deleteDiv.appendChild(delBtn); newBody.appendChild(deleteDiv); // 插入到header和add按钮之间 headerElement.parentNode.insertBefore(newBody, addageElement); }); } // 执行规则 function beibeibeibei_processRules(message) { // 获取所有保存的规则 const rules = GM_getValue('rulesData', []); // 过滤规则 const filteredRules = rules.filter(item => { // 确保 item 是数组且数组不为空 if (!Array.isArray(item) || item.length < 8) return false; // 检查第 0 项是否匹配 message const messageMatch = item[0] === message; // 检查 URL 是否包含第 1 项 const urlMatch = urlContainsText(item[1]); // 检查元素数量是否满足条件 const selector2 = item[2]; // 第2项是选择器 const condition = item[3]; // 第3项是条件 const elementCountMatch = checkElementCount(selector2, condition); // 检查第4项选择器的数量是否大于等于第5项 const selector4 = item[4]; // 第4项是选择器 const selector4Count = parseInt(item[5]); // 第5项是第几项(从0开始) const selector4CountMatch = checkElementCount(selector4, `最少 ${selector4Count + 1} 个`); // 返回所有条件都满足的规则 // console.log({messageMatch, urlMatch, elementCountMatch, selector4CountMatch}); return messageMatch && urlMatch && elementCountMatch && selector4CountMatch; }); console.log({ 0: 'beibeibeibei_processRules的filteredRules结果', message, filteredRules }); // 执行符合条件的规则 filteredRules.forEach((rule) => { const elements = document.querySelectorAll(rule[4]); if (elements.length > parseInt(rule[5])) { const targetIndex = parseInt(rule[5]); const targetElement = elements[targetIndex]; // 执行操作 switch (rule[6]) { case '点击':{ if (typeof targetElement.click === 'function') { targetElement.click(); } else { targetElement.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); } break; } case '旋转':{ const rotateValue = parseInt(rule[7]) || 0; let targetElementRotate = parseInt(getComputedStyle(targetElement).rotate) || 0; targetElement.style.rotate = `${(targetElementRotate + rotateValue) % 360}deg`; break; } case '缩放':{ const scaleValue = rule[7]; targetElement.style.transform = `scale(${scaleValue})`; break; } } } }); } // 正则匹配当前网址 function urlMatchesPattern(pattern) { const currentUrl = window.location.href; const regex = new RegExp(pattern); return regex.test(currentUrl); } // 普通文本匹配当前网址 function urlContainsText(text) { const currentUrl = window.location.href; return currentUrl.includes(text); } // 检查元素数量 function checkElementCount(selector, condition) { if (condition === '无限制') { // 无限制的意思就是无限制, 有没有元素都行, 选择器无效都行 return true; } // 验证选择器是否有效 if (!selector || typeof selector !== 'string' || selector.trim() === '') { // console.error('无效的选择器:', selector); return false; // 或抛出更明确的错误 } // 获取匹配指定 CSS 选择器的元素个数 const elements = document.querySelectorAll(selector); const count = elements.length; // 解析条件 let min = 0; let max = Infinity; let exact = null; let range = null; // 正则表达式用于提取数字 const numberRegex = /\d+/g; // 根据条件类型解析 if (condition === '无限制') { // 无限制的意思就是无限制, 有没有元素都行 return true; } else if (condition.startsWith('最少')) { // 最少 N 个 const numbers = condition.match(numberRegex); if (numbers && numbers.length > 0) { min = parseInt(numbers[0]); return count >= min; } } else if (condition.startsWith('最多')) { // 最多 N 个 const numbers = condition.match(numberRegex); if (numbers && numbers.length > 0) { max = parseInt(numbers[0]); return count <= max; } } else if (condition.startsWith('等于')) { // 等于 N 个 const numbers = condition.match(numberRegex); if (numbers && numbers.length > 0) { exact = parseInt(numbers[0]); return count === exact; } } else if (condition.startsWith('范围')) { // 范围 A-B 个 const numbers = condition.match(numberRegex); if (numbers && numbers.length === 2) { min = parseInt(numbers[0]); max = parseInt(numbers[1]); // 如果颠倒, 自动调换 if (min > max) { [min, max] = [max, min]; } return count >= min && count <= max; } } else { console.error('不支持的条件格式'); return false; } return false; } runWhenDocumentReady(add_HTML); runWhenDocumentReady(beibeibeibei_socket, beibeibeibei_restoreRulesData, beibeibeibei_init); runWhenDocumentReady(add_css); // // 用于临时测试 // setTimeout(() => { // // 添加测试目标元素 // const testArea = document.createElement("div"); // testArea.className = "test-area"; // 添加样式类 // testArea.id = "test-area"; // testArea.style.position = 'fixed'; // testArea.style.top = '200px'; // testArea.style.left = '200px'; // testArea.style.width = "300px"; // testArea.style.height = "200px"; // testArea.style.background = "linear-gradient(135deg,#FFD26F 15%,#3677FF 100%)"; // testArea.style.padding = "20px"; // testArea.style.display = "flex"; // testArea.style.flexWrap = "wrap"; // testArea.style.justifyContent = "flex-start"; // testArea.style.alignItems = "flex-start"; // document.body.appendChild(testArea); // // 添加一些可点击、可旋转、可缩放的元素 // for (let i = 0; i < 6; i++) { // const testElement = document.createElement("div"); // testElement.className = "test-element"; // testElement.style.margin = "10px"; // testElement.style.width = "80px"; // testElement.style.height = "60px"; // testElement.style.display = "flex"; // testElement.style.justifyContent = "center"; // testElement.style.alignItems = "center"; // testElement.style.backgroundColor = "#e0e0e0"; // testElement.style.borderRadius = "5px"; // testElement.style.boxShadow = "0 1px 3px rgba(0, 0, 0, 0.2)"; // testElement.style.cursor = "pointer"; // testElement.style.fontSize = "14px"; // testElement.style.transition = "all 0.3s ease"; // testElement.innerHTML = `测试元素 ${i + 1}`; // testElement.addEventListener("click", function () { // console.log(`测试元素 ${i + 1} 被点击了`); // }); // testArea.appendChild(testElement); // } // document.querySelector("#beibeibeibei-panel > div.titlebar > button.rule").click(); // beibeibeibei_processRules('msg-test'); // }, 40); })();